using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Shell32;

using WshShell = IWshRuntimeLibrary.WshShell;
using IWshShortcut = IWshRuntimeLibrary.IWshShortcut;
using System.Threading;

namespace ipzip
{

    public delegate void ProgressInfo(int percentage);

    public class Zipper : IDisposable
    {
        string zipFilename;

        string[] initialFilenames;

        bool atWork = false;
        bool stopRequested = false;

        ProgressInfo progressInfoCb;
        public ProgressInfo ProgressInfoCb
        {
            get { return this.progressInfoCb; }
            set { this.progressInfoCb = value; }
        }

        List<string> filenamesToInclude;
        public List<string> FilenamesToInclude
        {
            get { return this.filenamesToInclude; }
        }

        List<string> directoriesScanned;

        public Zipper(string[] filenames)
        {
            if(filenames.Length == 0) throw new ArgumentException(
                  "No filenames provided");
            this.initialFilenames = filenames;
            this.filenamesToInclude = new List<string>();
            this.directoriesScanned = new List<string>();
        }

        public void ScanAndZipFiles()
        {
            lock(this)
            {
                this.atWork = true;
            }
            try
            {
                this.CreateZip();
                this.CreateTempHierarchy();
                for(int i = 0; i < this.initialFilenames.Length;i++)
                {
                    string fn = this.initialFilenames[i];
                    this.Scan(fn,".");
                    if(this.progressInfoCb != null) this.progressInfoCb((int)(
                          (i+1.0) / this.initialFilenames.Length * 50));
                }
                this.SaveTempHierarchyToZip();
                if(this.progressInfoCb != null) this.progressInfoCb(100);
            }
            catch(Exception)
            {
                Trace.TraceInformation("Exception in ScanAndZipFiles()");
                if(File.Exists(this.zipFilename))
                {
                    try
                    {
                        File.Delete(this.zipFilename);
                    }
                    catch(Exception)
                    {
                    }
                }
                throw;
            }
            finally
            {
                lock(this)
                {
                    this.atWork = false;
                }
            }
        }

        /* Also see http://stackoverflow.com/questions/3282418 for 
         * SHFileOperation examples */

        //[Flags]
        //public enum FileOperationFlags : ushort
        //{
        //    FOF_NOERRORUI = 0x0400,
        //}

        [Flags]
        public enum SHContF : uint
        {
            SHCONTF_INCLUDEHIDDEN = 0x80,
            SHCONTF_NONFOLDERS = 0x40,
            SHCONTF_FOLDERS = 0x20,
        }

        static string ResolveLink(string s)
        {
            WshShell shell = new WshShell();
            Trace.TraceInformation("Resolving " + s);

            if(s.ToLower().EndsWith(".lnk"))
            {
                try
                {
                    IWshShortcut link = (IWshShortcut)shell.CreateShortcut(s);
                    //Trace.TraceInformation("Target path of shortcut " + s + " is: " +
                    //    link.TargetPath);
                    return link.TargetPath;
                }
                catch(COMException)
                {
                }
            }
            return s;
        }

        void Scan(string filename,string pathInZipFile)
        {
            lock(this)
            {
                if(this.stopRequested) return;
            }

            string resolvedFn = null;
            resolvedFn = ResolveLink(filename);
            if(Directory.Exists(resolvedFn))
            {
                if(!this.directoriesScanned.Contains(resolvedFn))
                {
                    this.directoriesScanned.Add(resolvedFn);
                    string[] entries = Directory.GetFileSystemEntries(resolvedFn);
                    foreach(string entry in entries)
                    {
                        if(!this.filenamesToInclude.Contains(entry))
                        {
                            this.Scan(entry,Path.Combine(pathInZipFile,
                                Path.GetFileNameWithoutExtension(filename) +
                                Path.GetExtension(resolvedFn)));
                        }
                    }
                }
            }
            else
            {
                this.SaveToTempHierarchy(resolvedFn,pathInZipFile,
                    Path.GetFileNameWithoutExtension(filename)+
                    Path.GetExtension(resolvedFn));
            }
        }

        private string tmpPath;

        void CreateTempHierarchy()
        {
            do
            {
                tmpPath = Path.Combine(Path.GetTempPath(),Path.GetRandomFileName());
            }
            while(Directory.Exists(tmpPath) || File.Exists(tmpPath));
            Trace.TraceInformation("Temporary directory is: " + tmpPath);
        }

        void SaveToTempHierarchy(string sourceFilename,string pathInZipFile,
            string filenameInZipFile)
        {
            string sourceFolderName = Path.GetDirectoryName(sourceFilename);
            string sourceFileBasename = Path.GetFileName(sourceFilename);

            string destPath = Path.Combine(tmpPath,pathInZipFile);
            Directory.CreateDirectory(destPath);
            string destFilePath = Path.Combine(destPath,filenameInZipFile);
            File.Copy(sourceFilename,destFilePath);

            Trace.TraceInformation("Copied " + sourceFilename + " to " + destPath);
        }

        void SaveTempHierarchyToZip()
        {
            Shell32.Folder zipFolder = GetShell32NameSpace(zipFilename);
            if(!Directory.Exists(tmpPath))
            {
                throw new InvalidOperationException("Source folder has no items");
            }
            Shell32.Folder3 sourceFolder = (Shell32.Folder3)GetShell32NameSpace(tmpPath);

            Shell32.FolderItems3 folderItems = (Shell32.FolderItems3)
                sourceFolder.Items();
            Trace.TraceInformation("Folder item count in " + tmpPath + ": " +
                folderItems.Count);

            folderItems.Filter((int)
                (SHContF.SHCONTF_FOLDERS |
                 SHContF.SHCONTF_NONFOLDERS |
                 SHContF.SHCONTF_INCLUDEHIDDEN),"*");
            for(int i=0;i<folderItems.Count;i++)
            {
                lock(this)
                {
                    if(this.stopRequested) return;
                }
                Shell32.FolderItem folderItem = folderItems.Item(i);
                int oldItemCount = zipFolder.Items().Count;
                Trace.TraceInformation("Item count immediately before CopyHere: " +
                    zipFolder.Items().Count);
                if(folderItem.IsFolder)
                {
                    Shell32.Folder f = GetShell32NameSpace(folderItem.Path);
                    zipFolder.CopyHere(f,0);
                }
                else
                {
                    zipFolder.CopyHere(folderItem,0);
                }
                Trace.TraceInformation("Item count immediately after CopyHere: " +
                    zipFolder.Items().Count);
                bool lockSeen = false;
                do
                {
                    /* Don't make this delay too long so that the user doesn't
                     * have the chance to hit 'Cancel' too early */
                    Thread.Sleep(100);
                    try
                    {
                        using(FileStream fs = new FileStream(zipFilename,
                            FileMode.Open));
                        /* File is now not locked anymore by the Windows zipper,
                         * we expect that its item count must be raised. But
                         * only do so if we have seen at least once that the file 
                         * was locked, otherwise we might run in danger that
                         * the zipper hasn't even begun yet. */
                        if(lockSeen && oldItemCount >= zipFolder.Items().Count)
                        {
                            Trace.TraceError("User canceled ZIP operation: "+
                                "zipFolder.Items().Count="+
                                zipFolder.Items().Count);
                            throw new OperationCanceledException(
                                "User canceled ZIP operation");
                        }
                    }
                    catch(IOException)
                    {
                        lockSeen = true;
                    }
                }
                /* Need >= condition because Count is temporarily 0 during the
                 * operation: */
                while(oldItemCount >= zipFolder.Items().Count); 
                if(this.progressInfoCb != null) this.progressInfoCb(
                      (50*(i + 1) / folderItems.Count) + 50);
            }
        }

        public static readonly byte[] EmptyZipFileContent=
            { 80,75,5,6,0,0,0,0,
              0,0,0,0,0,0,0,0,
              0,0,0,0,0,0 };

        /**
         * <exception cref="IOException"></exception>
         */
        public void CreateZip()
        {
            string zipFileBasename = this.initialFilenames[0];
            /* In case zipFileBasename refers not to a directory, 
             * but to a file, remove its extension because we append
             * ".zip" later to get the ZIP file name */
            if (!Directory.Exists(zipFileBasename))
            {
                zipFileBasename = Path.Combine(
                    Path.GetDirectoryName(zipFileBasename),
                    Path.GetFileNameWithoutExtension(zipFileBasename));
            }
            string initialZipFileBasename = zipFileBasename;

            bool fileCouldBeCreated = false;
            for(int i=0;!fileCouldBeCreated && i<1000;i++)
            {
                zipFilename = Path.GetFullPath(zipFileBasename + ".zip");
                Trace.TraceInformation("Trying to create create ZIP file " + zipFilename);
                try
                {
                    /* FileMode.CreateNew generates IOException if the file exists
                     * already, in which case we loop for some time trying new filenames */
                    using(FileStream fs = new FileStream(zipFilename,FileMode.CreateNew))
                    {
                        fileCouldBeCreated = true;
                        fs.Write(EmptyZipFileContent,0,EmptyZipFileContent.Length);
                    }
                }
                catch(IOException)
                {
                    /* Check if the exception was likely due to an already existing
                     * file *before* we created it as ZIP file. If not, rethrow. */
                    if(fileCouldBeCreated || !File.Exists(zipFilename))
                    {
                        throw;
                    }
                    zipFileBasename = initialZipFileBasename + " (" + (i+1) + ")";
                }
            }
            
        }

        /* Creates Folder objects with reflection so that it works 
         * independent of Windows version */
        public Shell32.Folder GetShell32NameSpace(Object folder)
        {
            Type shellAppType = Type.GetTypeFromProgID("Shell.Application");
            Object shell = Activator.CreateInstance(shellAppType);
            return (Shell32.Folder)shellAppType.InvokeMember("NameSpace",
                System.Reflection.BindingFlags.InvokeMethod,null,shell,
                new object[] { folder });
        }

        #region IDisposable Members

        public void Dispose()
        {
            do
            {
                lock(this)
                {
                    if(!this.atWork) break;
                    this.stopRequested = true;
                    Trace.TraceInformation("Waiting for zipper to complete");
                }
                Thread.Sleep(1000);
            }
            while(true);

            if(Directory.Exists(this.tmpPath))
            {
                try
                {
                    Trace.TraceInformation("Deleting temporary files: " +
                        this.tmpPath);
                    Directory.Delete(this.tmpPath,true);
                }
                catch(Exception e0)
                {
                    Trace.TraceError("Cannot delete temporary files: " + e0);
                }
            }
        }

        #endregion
    }
}
